package com.appspot.web2stanza;

import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.CRC32;

import com.google.appengine.api.datastore.*;
import com.google.appengine.api.urlfetch.ResponseTooLargeException;
import org.mozilla.universalchardet.UniversalDetector;
import com.appspot.web2stanza.velocity.*;

class Web2Stanza {

  private static final String EPUB_KIND = "epub";
  private static final String EPUB_DATA = "epub_data";
  private static final String EPUB_TIME = "epub_time";

  private DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
  private String id;
  private String url;
  private String lang;

  // Constructor
  public Web2Stanza(String id, String url, String lang) {
    this.id = id;
    this.url = url;
    this.lang = lang;
  }

  public Web2Stanza(String id) {
    this.id = id;
  }

  public Web2Stanza() {
  }

  /**
   * Build epub file.
   */
  public int execute(byte[] source) throws IOException, Exception {

    // detect encoding
    UniversalDetector detector = new UniversalDetector(null);
    detector.handleData(source, 0, source.length);
    detector.dataEnd();
    String encoding = detector.getDetectedCharset();
    detector.reset();

    // create zip file
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    ZipOutputStream zos = new ZipOutputStream(os);
    ZipEntry ze = null;

    // create mimetype first
    addMimetype(zos);

    // construct xhtml
    InputStream srcIs = new ByteArrayInputStream(source);
    Reader reader = null;
    if (encoding != null) {
      reader = new InputStreamReader(srcIs, encoding);
    } else {
      reader = new InputStreamReader(srcIs, Builder.ENCODING);
    }

    Epub epub = new Epub(id);

    Item index = new Item();
    index.setId("index");
    index.setHref("index.xhtml");
    index.setMediaType("application/xhtml+xml");
    epub.getItemList().add(index);

    String xhtmlPath = "OEBPS/" + index.getHref();
    ze = new ZipEntry(xhtmlPath);
    zos.putNextEntry(ze);

    try {
      HTMLParser parser = new HTMLParser(reader, lang);

      parser.setInfo(epub);

      parser.html2Xhtml(zos);

    } finally {
      srcIs.close();
    }

    // add external files
    //addExternals(epub, zos);

    // add guide files
    //addGuide(epub, zos);

    // construct epub files
    Builder builder = new Builder("epub", epub);
    OutputStreamWriter zosw = new OutputStreamWriter(zos, Builder.ENCODING);
    BufferedWriter zbw = new BufferedWriter(zosw);

    String containerPath = "META-INF/container.xml";
    mergeTemplate(builder, containerPath, zos, zbw);

    String opfPath = "OEBPS/content.opf";
    mergeTemplate(builder, opfPath, zos, zbw);

    String ncxPath = "OEBPS/toc.ncx";
    mergeTemplate(builder, ncxPath, zos, zbw);

    // save
    zbw.close();
    zos.close();
    String key = saveEpub(os.toByteArray());

    MainServlet.log.info("Create => " + key);

    return os.size();
  }

  /**
   * Create mimetype in zip file.
   */
  private void addMimetype(ZipOutputStream zos) throws IOException {
    zos.setMethod(ZipOutputStream.STORED);

    byte[] data = "application/epub+zip".getBytes();
    ZipEntry ze = new ZipEntry("mimetype");

    ze.setSize(data.length);
    CRC32 crc = new CRC32();
    crc.update(data);
    ze.setCrc(crc.getValue());

    zos.putNextEntry(ze);
    zos.write(data);

    zos.setMethod(ZipOutputStream.DEFLATED);
  }

  /**
   * fetch and add external files
   */
  private void addExternals(Epub epub, ZipOutputStream zos) throws IOException {
    List<Item> list = epub.getItemList();
    for (Iterator<Item> it = list.iterator(); it.hasNext();) {
      Item item = it.next();
      if (item.getIsExternal()) {
        String path = item.getHref();

        // get base url
        URL u = new URL(url);
        URL external = new URL(u, path);

        byte[] data = null;
        try {
          data = GAEFetch.fetch(external);
        } catch (Exception e) {
          // remove item
          it.remove();

          continue;
        }

        String entryPoint = path;
        if (!entryPoint.startsWith("/")) {
          entryPoint = "OEBPS/" + entryPoint;
        }
        ZipEntry ze = new ZipEntry(entryPoint);
        zos.putNextEntry(ze);
        zos.write(data);
      }
    }
  }

  /**
   * add guide files
   */
  private void addGuide(Epub epub, ZipOutputStream zos) throws Exception {
    if (url.startsWith("http://www.aozora.gr.jp")) {
      // add guide element to epub object
      List<GuideRef> guideList = epub.getGuideList();
      GuideRef ref = new GuideRef();
      ref.setType("cover");
      ref.setTitle("Cover Image");
      ref.setHref("guide/cover.xhtml");
      guideList.add(ref);

      // add item to epub item list
      List<Item> itemList = epub.getItemList();

      Item image = new Item();
      image.setId("cover.image");
      image.setHref("guide/aozora.png");
      image.setMediaType("image/png");
      image.setIsToc(false);
      itemList.add(image);

      Item xhtml = new Item();
      xhtml.setId("cover");
      xhtml.setHref("guide/cover.xhtml");
      xhtml.setMediaType("application/xhtml+xml");
      itemList.add(0, xhtml);

      // add aozora cover image to epub zip
      String path = "OEBPS/guide/aozora.png";
      ZipEntry ze = new ZipEntry(path);
      zos.putNextEntry(ze);
      File aozoraCover = new File("template/" + path);
      FileInputStream fis = new FileInputStream(aozoraCover);
      byte[] buffer = new byte[1024];
      while (fis.read(buffer) > 0) {
        zos.write(buffer);
      }
      fis.close();

      Cover cover = new Cover("aozora.png", "cover");
      Builder builder = new Builder("cover", cover);
      OutputStreamWriter zosw = new OutputStreamWriter(zos, Builder.ENCODING);
      mergeTemplate(builder, "OEBPS/guide/cover.xhtml", zos, zosw);
    }
  }

  /**
   * Build ePub contents with Velocity
   */
  private void mergeTemplate(Builder builder, String path, ZipOutputStream zos, Writer writer) throws Exception {
    ZipEntry ze = new ZipEntry(path);
    zos.putNextEntry(ze);
    builder.mergeTemplate(path, writer);
    writer.flush();
  }

  /**
   * save ePub to Datastore
   */
  private String saveEpub(byte[] data) {
    Entity e = new Entity(EPUB_KIND, id);
    e.setProperty(EPUB_DATA, new Blob(data));
    e.setProperty(EPUB_TIME, System.currentTimeMillis());
    Key key = ds.put(e);
    return key.toString();
  }

  /**
   * download epub file
   */
  public void download(OutputStream os) throws Exception {
    byte[] buff = new byte[1024];

    Entity e = ds.get(KeyFactory.createKey(EPUB_KIND, id));
    Blob b = (Blob) e.getProperty(EPUB_DATA);
    InputStream is = new ByteArrayInputStream(b.getBytes());

    try {
      while (true) {
        int len = is.read(buff);
        if (len < 0) break;
        os.write(buff, 0, len);
      }
    } finally {
      is.close();
    }
  }

  /**
   * delete epub entity
   */
  public void cleaning() {
    Query query = new Query(EPUB_KIND);
    query.addFilter(
        EPUB_TIME,
        Query.FilterOperator.LESS_THAN,
        System.currentTimeMillis() - 1800000);
    List<Entity> epubs = ds.prepare(query).asList(
        FetchOptions.Builder.withOffset(0));
    int count = epubs.size();
    for (Entity e : epubs) {
      ds.delete(e.getKey());
    }

    CleaningJob.log.info("CleaningJob delete count => " + count);
  }

  /**
   * delete id directory
   */
  public void cleaning(String id) {
    ds.delete(KeyFactory.createKey(EPUB_KIND, id));

    CleaningJob.log.info("CleaningJob delete => " + id);
  }

}
